/**
 * Copyright (c) 2011-2013 Fabien Cazenave, Mozilla.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
/*
 * Additional modifications for PDF.js project: - Disables language
 * initialization on page loading; - Removes consoleWarn and consoleLog and use
 * console.log/warn directly. - Removes window._ assignment. - Remove
 * compatibility code for OldIE.
 */

/* jshint browser: true, devel: true, es5: true, globalstrict: true */
'use strict';

document.webL10n = (function(window, document, undefined) {
	var gL10nData = {};
	var gTextData = '';
	var gTextProp = 'textContent';
	var gLanguage = '';
	var gMacros = {};
	var gReadyState = 'loading';

	/**
	 * Synchronously loading l10n resources significantly minimizes flickering
	 * from displaying the app with non-localized strings and then updating the
	 * strings. Although this will block all script execution on this page, we
	 * expect that the l10n resources are available locally on flash-storage.
	 * 
	 * As synchronous XHR is generally considered as a bad idea, we're still
	 * loading l10n resources asynchronously -- but we keep this in a setting,
	 * just in case... and applications using this library should hide their
	 * content until the `localized' event happens.
	 */

	var gAsyncResourceLoading = true; // read-only

	/**
	 * DOM helpers for the so-called "HTML API".
	 * 
	 * These functions are written for modern browsers. For old versions of IE,
	 * they're overridden in the 'startup' section at the end of this file.
	 */

	function getL10nResourceLinks() {
		return document.querySelectorAll('link[type="application/l10n"]');
	}

	function getL10nDictionary() {
		var script = document.querySelector('script[type="application/l10n"]');
		// TODO: support multiple and external JSON dictionaries
		return script ? JSON.parse(script.innerHTML) : null;
	}

	function getTranslatableChildren(element) {
		return element ? element.querySelectorAll('*[data-l10n-id]') : [];
	}

	function getL10nAttributes(element) {
		if (!element)
			return {};

		var l10nId = element.getAttribute('data-l10n-id');
		var l10nArgs = element.getAttribute('data-l10n-args');
		var args = {};
		if (l10nArgs) {
			try {
				args = JSON.parse(l10nArgs);
			} catch (e) {
				console.warn('could not parse arguments for #' + l10nId);
			}
		}
		return {
			id : l10nId,
			args : args
		};
	}

	function fireL10nReadyEvent(lang) {
		var evtObject = document.createEvent('Event');
		evtObject.initEvent('localized', true, false);
		evtObject.language = lang;
		document.dispatchEvent(evtObject);
	}

	function xhrLoadText(url, onSuccess, onFailure) {
		onSuccess = onSuccess || function _onSuccess(data) {
		};
		onFailure = onFailure || function _onFailure() {
			console.warn(url + ' not found.');
		};

		var xhr = new XMLHttpRequest();
		xhr.open('GET', url, gAsyncResourceLoading);
		if (xhr.overrideMimeType) {
			xhr.overrideMimeType('text/plain; charset=utf-8');
		}
		xhr.onreadystatechange = function() {
			if (xhr.readyState == 4) {
				if (xhr.status == 200 || xhr.status === 0) {
					onSuccess(xhr.responseText);
				} else {
					onFailure();
				}
			}
		};
		xhr.onerror = onFailure;
		xhr.ontimeout = onFailure;

		// in Firefox OS with the app:// protocol, trying to XHR a non-existing
		// URL will raise an exception here -- hence this ugly try...catch.
		try {
			xhr.send(null);
		} catch (e) {
			onFailure();
		}
	}

	/**
	 * l10n resource parser: - reads (async XHR) the l10n resource matching
	 * `lang'; - imports linked resources (synchronously) when specified; -
	 * parses the text data (fills `gL10nData' and `gTextData'); - triggers
	 * success/failure callbacks when done.
	 * 
	 * @param {string}
	 *            href URL of the l10n resource to parse.
	 * 
	 * @param {string}
	 *            lang locale (language) to parse. Must be a lowercase string.
	 * 
	 * @param {Function}
	 *            successCallback triggered when the l10n resource has been
	 *            successully parsed.
	 * 
	 * @param {Function}
	 *            failureCallback triggered when the an error has occured.
	 * 
	 * @return {void} uses the following global variables: gL10nData, gTextData,
	 *         gTextProp.
	 */

	function parseResource(href, lang, successCallback, failureCallback) {
		var baseURL = href.replace(/[^\/]*$/, '') || './';

		// handle escaped characters (backslashes) in a string
		function evalString(text) {
			if (text.lastIndexOf('\\') < 0)
				return text;
			return text.replace(/\\\\/g, '\\').replace(/\\n/g, '\n').replace(
					/\\r/g, '\r').replace(/\\t/g, '\t').replace(/\\b/g, '\b')
					.replace(/\\f/g, '\f').replace(/\\{/g, '{').replace(/\\}/g,
							'}').replace(/\\"/g, '"').replace(/\\'/g, "'");
		}

		// parse *.properties text data into an l10n dictionary
		// If gAsyncResourceLoading is false, then the callback will be called
		// synchronously. Otherwise it is called asynchronously.
		function parseProperties(text, parsedPropertiesCallback) {
			var dictionary = {};

			// token expressions
			var reBlank = /^\s*|\s*$/;
			var reComment = /^\s*#|^\s*$/;
			var reSection = /^\s*\[(.*)\]\s*$/;
			var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
			var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with
													// '\'

			// parse the *.properties file into an associative array
			function parseRawLines(rawText, extendedSyntax,
					parsedRawLinesCallback) {
				var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
				var currentLang = '*';
				var genericLang = lang.split('-', 1)[0];
				var skipLang = false;
				var match = '';

				function nextEntry() {
					// Use infinite loop instead of recursion to avoid reaching
					// the
					// maximum recursion limit for content with many lines.
					while (true) {
						if (!entries.length) {
							parsedRawLinesCallback();
							return;
						}
						var line = entries.shift();

						// comment or blank line?
						if (reComment.test(line))
							continue;

						// the extended syntax supports [lang] sections and
						// @import rules
						if (extendedSyntax) {
							match = reSection.exec(line);
							if (match) { // section start?
								// RFC 4646, section 4.4, "All comparisons MUST
								// be performed
								// in a case-insensitive manner."

								currentLang = match[1].toLowerCase();
								skipLang = (currentLang !== '*')
										&& (currentLang !== lang)
										&& (currentLang !== genericLang);
								continue;
							} else if (skipLang) {
								continue;
							}
							match = reImport.exec(line);
							if (match) { // @import rule?
								loadImport(baseURL + match[1], nextEntry);
								return;
							}
						}

						// key-value pair
						var tmp = line.match(reSplit);
						if (tmp && tmp.length == 3) {
							dictionary[tmp[1]] = evalString(tmp[2]);
						}
					}
				}
				nextEntry();
			}

			// import another *.properties file
			function loadImport(url, callback) {
				xhrLoadText(url, function(content) {
							parseRawLines(content, false, callback); // don't
																		// allow
																		// recursive
																		// imports
						}, null);
			}

			// fill the dictionary
			parseRawLines(text, true, function() {
						parsedPropertiesCallback(dictionary);
					});
		}

		// load and parse l10n data (warning: global variables are used here)
		xhrLoadText(href, function(response) {
					gTextData += response; // mostly for debug

					// parse *.properties text data into an l10n dictionary
					parseProperties(response, function(data) {

								// find attribute descriptions, if any
								for (var key in data) {
									var id, prop, index = key.lastIndexOf('.');
									if (index > 0) { // an attribute has been
														// specified
										id = key.substring(0, index);
										prop = key.substr(index + 1);
									} else { // no attribute: assuming text
												// content by default
										id = key;
										prop = gTextProp;
									}
									if (!gL10nData[id]) {
										gL10nData[id] = {};
									}
									gL10nData[id][prop] = data[key];
								}

								// trigger callback
								if (successCallback) {
									successCallback();
								}
							});
				}, failureCallback);
	}

	// load and parse all resources for the specified locale
	function loadLocale(lang, callback) {
		// RFC 4646, section 2.1 states that language tags have to be treated as
		// case-insensitive. Convert to lowercase for case-insensitive
		// comparisons.
		if (lang) {
			lang = lang.toLowerCase();
		}

		callback = callback || function _callback() {
		};

		clear();
		gLanguage = lang;

		// check all <link type="application/l10n" href="..." /> nodes
		// and load the resource files
		var langLinks = getL10nResourceLinks();
		var langCount = langLinks.length;
		if (langCount === 0) {
			// we might have a pre-compiled dictionary instead
			var dict = getL10nDictionary();
			if (dict && dict.locales && dict.default_locale) {
				console.log('using the embedded JSON directory, early way out');
				gL10nData = dict.locales[lang];
				if (!gL10nData) {
					var defaultLocale = dict.default_locale.toLowerCase();
					for (var anyCaseLang in dict.locales) {
						anyCaseLang = anyCaseLang.toLowerCase();
						if (anyCaseLang === lang) {
							gL10nData = dict.locales[lang];
							break;
						} else if (anyCaseLang === defaultLocale) {
							gL10nData = dict.locales[defaultLocale];
						}
					}
				}
				callback();
			} else {
				console.log('no resource to load, early way out');
			}
			// early way out
			fireL10nReadyEvent(lang);
			gReadyState = 'complete';
			return;
		}

		// start the callback when all resources are loaded
		var onResourceLoaded = null;
		var gResourceCount = 0;
		onResourceLoaded = function() {
			gResourceCount++;
			if (gResourceCount >= langCount) {
				callback();
				fireL10nReadyEvent(lang);
				gReadyState = 'complete';
			}
		};

		// load all resource files
		function L10nResourceLink(link) {
			var href = link.href;
			// Note: If |gAsyncResourceLoading| is false, then the following
			// callbacks
			// are synchronously called.
			this.load = function(lang, callback) {
				parseResource(href, lang, callback, function() {
							console.warn(href + ' not found.');
							// lang not found, used default resource instead
							console.warn('"' + lang + '" resource not found');
							gLanguage = '';
							// Resource not loaded, but we still need to call
							// the callback.
							callback();
						});
			};
		}

		for (var i = 0; i < langCount; i++) {
			var resource = new L10nResourceLink(langLinks[i]);
			resource.load(lang, onResourceLoaded);
		}
	}

	// clear all l10n data
	function clear() {
		gL10nData = {};
		gTextData = '';
		gLanguage = '';
		// TODO: clear all non predefined macros.
		// There's no such macro /yet/ but we're planning to have some...
	}

	/**
	 * Get rules for plural forms (shared with JetPack), see:
	 * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
	 * https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
	 * 
	 * @param {string}
	 *            lang locale (language) used.
	 * 
	 * @return {Function} returns a function that gives the plural form name for
	 *         a given integer: var fun = getPluralRules('en'); fun(1) -> 'one'
	 *         fun(0) -> 'other' fun(1000) -> 'other'.
	 */

	function getPluralRules(lang) {
		var locales2rules = {
			'af' : 3,
			'ak' : 4,
			'am' : 4,
			'ar' : 1,
			'asa' : 3,
			'az' : 0,
			'be' : 11,
			'bem' : 3,
			'bez' : 3,
			'bg' : 3,
			'bh' : 4,
			'bm' : 0,
			'bn' : 3,
			'bo' : 0,
			'br' : 20,
			'brx' : 3,
			'bs' : 11,
			'ca' : 3,
			'cgg' : 3,
			'chr' : 3,
			'cs' : 12,
			'cy' : 17,
			'da' : 3,
			'de' : 3,
			'dv' : 3,
			'dz' : 0,
			'ee' : 3,
			'el' : 3,
			'en' : 3,
			'eo' : 3,
			'es' : 3,
			'et' : 3,
			'eu' : 3,
			'fa' : 0,
			'ff' : 5,
			'fi' : 3,
			'fil' : 4,
			'fo' : 3,
			'fr' : 5,
			'fur' : 3,
			'fy' : 3,
			'ga' : 8,
			'gd' : 24,
			'gl' : 3,
			'gsw' : 3,
			'gu' : 3,
			'guw' : 4,
			'gv' : 23,
			'ha' : 3,
			'haw' : 3,
			'he' : 2,
			'hi' : 4,
			'hr' : 11,
			'hu' : 0,
			'id' : 0,
			'ig' : 0,
			'ii' : 0,
			'is' : 3,
			'it' : 3,
			'iu' : 7,
			'ja' : 0,
			'jmc' : 3,
			'jv' : 0,
			'ka' : 0,
			'kab' : 5,
			'kaj' : 3,
			'kcg' : 3,
			'kde' : 0,
			'kea' : 0,
			'kk' : 3,
			'kl' : 3,
			'km' : 0,
			'kn' : 0,
			'ko' : 0,
			'ksb' : 3,
			'ksh' : 21,
			'ku' : 3,
			'kw' : 7,
			'lag' : 18,
			'lb' : 3,
			'lg' : 3,
			'ln' : 4,
			'lo' : 0,
			'lt' : 10,
			'lv' : 6,
			'mas' : 3,
			'mg' : 4,
			'mk' : 16,
			'ml' : 3,
			'mn' : 3,
			'mo' : 9,
			'mr' : 3,
			'ms' : 0,
			'mt' : 15,
			'my' : 0,
			'nah' : 3,
			'naq' : 7,
			'nb' : 3,
			'nd' : 3,
			'ne' : 3,
			'nl' : 3,
			'nn' : 3,
			'no' : 3,
			'nr' : 3,
			'nso' : 4,
			'ny' : 3,
			'nyn' : 3,
			'om' : 3,
			'or' : 3,
			'pa' : 3,
			'pap' : 3,
			'pl' : 13,
			'ps' : 3,
			'pt' : 3,
			'rm' : 3,
			'ro' : 9,
			'rof' : 3,
			'ru' : 11,
			'rwk' : 3,
			'sah' : 0,
			'saq' : 3,
			'se' : 7,
			'seh' : 3,
			'ses' : 0,
			'sg' : 0,
			'sh' : 11,
			'shi' : 19,
			'sk' : 12,
			'sl' : 14,
			'sma' : 7,
			'smi' : 7,
			'smj' : 7,
			'smn' : 7,
			'sms' : 7,
			'sn' : 3,
			'so' : 3,
			'sq' : 3,
			'sr' : 11,
			'ss' : 3,
			'ssy' : 3,
			'st' : 3,
			'sv' : 3,
			'sw' : 3,
			'syr' : 3,
			'ta' : 3,
			'te' : 3,
			'teo' : 3,
			'th' : 0,
			'ti' : 4,
			'tig' : 3,
			'tk' : 3,
			'tl' : 4,
			'tn' : 3,
			'to' : 0,
			'tr' : 0,
			'ts' : 3,
			'tzm' : 22,
			'uk' : 11,
			'ur' : 3,
			've' : 3,
			'vi' : 0,
			'vun' : 3,
			'wa' : 4,
			'wae' : 3,
			'wo' : 0,
			'xh' : 3,
			'xog' : 3,
			'yo' : 0,
			'zh' : 0,
			'zu' : 3
		};

		// utility functions for plural rules methods
		function isIn(n, list) {
			return list.indexOf(n) !== -1;
		}
		function isBetween(n, start, end) {
			return start <= n && n <= end;
		}

		// list of all plural rules methods:
		// map an integer to the plural form name to use
		var pluralRules = {
			'0' : function(n) {
				return 'other';
			},
			'1' : function(n) {
				if ((isBetween((n % 100), 3, 10)))
					return 'few';
				if (n === 0)
					return 'zero';
				if ((isBetween((n % 100), 11, 99)))
					return 'many';
				if (n == 2)
					return 'two';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'2' : function(n) {
				if (n !== 0 && (n % 10) === 0)
					return 'many';
				if (n == 2)
					return 'two';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'3' : function(n) {
				if (n == 1)
					return 'one';
				return 'other';
			},
			'4' : function(n) {
				if ((isBetween(n, 0, 1)))
					return 'one';
				return 'other';
			},
			'5' : function(n) {
				if ((isBetween(n, 0, 2)) && n != 2)
					return 'one';
				return 'other';
			},
			'6' : function(n) {
				if (n === 0)
					return 'zero';
				if ((n % 10) == 1 && (n % 100) != 11)
					return 'one';
				return 'other';
			},
			'7' : function(n) {
				if (n == 2)
					return 'two';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'8' : function(n) {
				if ((isBetween(n, 3, 6)))
					return 'few';
				if ((isBetween(n, 7, 10)))
					return 'many';
				if (n == 2)
					return 'two';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'9' : function(n) {
				if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
					return 'few';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'10' : function(n) {
				if ((isBetween((n % 10), 2, 9))
						&& !(isBetween((n % 100), 11, 19)))
					return 'few';
				if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
					return 'one';
				return 'other';
			},
			'11' : function(n) {
				if ((isBetween((n % 10), 2, 4))
						&& !(isBetween((n % 100), 12, 14)))
					return 'few';
				if ((n % 10) === 0 || (isBetween((n % 10), 5, 9))
						|| (isBetween((n % 100), 11, 14)))
					return 'many';
				if ((n % 10) == 1 && (n % 100) != 11)
					return 'one';
				return 'other';
			},
			'12' : function(n) {
				if ((isBetween(n, 2, 4)))
					return 'few';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'13' : function(n) {
				if ((isBetween((n % 10), 2, 4))
						&& !(isBetween((n % 100), 12, 14)))
					return 'few';
				if (n != 1 && (isBetween((n % 10), 0, 1))
						|| (isBetween((n % 10), 5, 9))
						|| (isBetween((n % 100), 12, 14)))
					return 'many';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'14' : function(n) {
				if ((isBetween((n % 100), 3, 4)))
					return 'few';
				if ((n % 100) == 2)
					return 'two';
				if ((n % 100) == 1)
					return 'one';
				return 'other';
			},
			'15' : function(n) {
				if (n === 0 || (isBetween((n % 100), 2, 10)))
					return 'few';
				if ((isBetween((n % 100), 11, 19)))
					return 'many';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'16' : function(n) {
				if ((n % 10) == 1 && n != 11)
					return 'one';
				return 'other';
			},
			'17' : function(n) {
				if (n == 3)
					return 'few';
				if (n === 0)
					return 'zero';
				if (n == 6)
					return 'many';
				if (n == 2)
					return 'two';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'18' : function(n) {
				if (n === 0)
					return 'zero';
				if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
					return 'one';
				return 'other';
			},
			'19' : function(n) {
				if ((isBetween(n, 2, 10)))
					return 'few';
				if ((isBetween(n, 0, 1)))
					return 'one';
				return 'other';
			},
			'20' : function(n) {
				if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9))
						&& !(isBetween((n % 100), 10, 19)
								|| isBetween((n % 100), 70, 79) || isBetween(
								(n % 100), 90, 99)))
					return 'few';
				if ((n % 1000000) === 0 && n !== 0)
					return 'many';
				if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
					return 'two';
				if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
					return 'one';
				return 'other';
			},
			'21' : function(n) {
				if (n === 0)
					return 'zero';
				if (n == 1)
					return 'one';
				return 'other';
			},
			'22' : function(n) {
				if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
					return 'one';
				return 'other';
			},
			'23' : function(n) {
				if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
					return 'one';
				return 'other';
			},
			'24' : function(n) {
				if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
					return 'few';
				if (isIn(n, [2, 12]))
					return 'two';
				if (isIn(n, [1, 11]))
					return 'one';
				return 'other';
			}
		};

		// return a function that gives the plural form name for a given integer
		var index = locales2rules[lang.replace(/-.*$/, '')];
		if (!(index in pluralRules)) {
			console.warn('plural form unknown for [' + lang + ']');
			return function() {
				return 'other';
			};
		}
		return pluralRules[index];
	}

	// pre-defined 'plural' macro
	gMacros.plural = function(str, param, key, prop) {
		var n = parseFloat(param);
		if (isNaN(n))
			return str;

		// TODO: support other properties (l20n still doesn't...)
		if (prop != gTextProp)
			return str;

		// initialize _pluralRules
		if (!gMacros._pluralRules) {
			gMacros._pluralRules = getPluralRules(gLanguage);
		}
		var index = '[' + gMacros._pluralRules(n) + ']';

		// try to find a [zero|one|two] key if it's defined
		if (n === 0 && (key + '[zero]') in gL10nData) {
			str = gL10nData[key + '[zero]'][prop];
		} else if (n == 1 && (key + '[one]') in gL10nData) {
			str = gL10nData[key + '[one]'][prop];
		} else if (n == 2 && (key + '[two]') in gL10nData) {
			str = gL10nData[key + '[two]'][prop];
		} else if ((key + index) in gL10nData) {
			str = gL10nData[key + index][prop];
		} else if ((key + '[other]') in gL10nData) {
			str = gL10nData[key + '[other]'][prop];
		}

		return str;
	};

	/**
	 * l10n dictionary functions
	 */

	// fetch an l10n object, warn if not found, apply `args' if possible
	function getL10nData(key, args, fallback) {
		var data = gL10nData[key];
		if (!data) {
			console.warn('#' + key + ' is undefined.');
			if (!fallback) {
				return null;
			}
			data = fallback;
		}

		/**
		 * This is where l10n expressions should be processed. The plan is to
		 * support C-style expressions from the l20n project; until then, only
		 * two kinds of simple expressions are supported: {[ index ]} and {{
		 * arguments }}.
		 */
		var rv = {};
		for (var prop in data) {
			var str = data[prop];
			str = substIndexes(str, args, key, prop);
			str = substArguments(str, args, key);
			rv[prop] = str;
		}
		return rv;
	}

	// replace {[macros]} with their values
	function substIndexes(str, args, key, prop) {
		var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
		var reMatch = reIndex.exec(str);
		if (!reMatch || !reMatch.length)
			return str;

		// an index/macro has been found
		// Note: at the moment, only one parameter is supported
		var macroName = reMatch[1];
		var paramName = reMatch[2];
		var param;
		if (args && paramName in args) {
			param = args[paramName];
		} else if (paramName in gL10nData) {
			param = gL10nData[paramName];
		}

		// there's no macro parser yet: it has to be defined in gMacros
		if (macroName in gMacros) {
			var macro = gMacros[macroName];
			str = macro(str, param, key, prop);
		}
		return str;
	}

	// replace {{arguments}} with their values
	function substArguments(str, args, key) {
		var reArgs = /\{\{\s*(.+?)\s*\}\}/g;
		return str.replace(reArgs, function(matched_text, arg) {
					if (args && arg in args) {
						return args[arg];
					}
					if (arg in gL10nData) {
						return gL10nData[arg];
					}
					console.log('argument {{' + arg + '}} for #' + key
							+ ' is undefined.');
					return matched_text;
				});
	}

	// translate an HTML element
	function translateElement(element) {
		var l10n = getL10nAttributes(element);
		if (!l10n.id)
			return;

		// get the related l10n object
		var data = getL10nData(l10n.id, l10n.args);
		if (!data) {
			console.warn('#' + l10n.id + ' is undefined.');
			return;
		}

		// translate element (TODO: security checks?)
		if (data[gTextProp]) { // XXX
			if (getChildElementCount(element) === 0) {
				element[gTextProp] = data[gTextProp];
			} else {
				// this element has element children: replace the content of the
				// first
				// (non-empty) child textNode and clear other child textNodes
				var children = element.childNodes;
				var found = false;
				for (var i = 0, l = children.length; i < l; i++) {
					if (children[i].nodeType === 3
							&& /\S/.test(children[i].nodeValue)) {
						if (found) {
							children[i].nodeValue = '';
						} else {
							children[i].nodeValue = data[gTextProp];
							found = true;
						}
					}
				}
				// if no (non-empty) textNode is found, insert a textNode before
				// the
				// first element child.
				if (!found) {
					var textNode = document.createTextNode(data[gTextProp]);
					element.insertBefore(textNode, element.firstChild);
				}
			}
			delete data[gTextProp];
		}

		for (var k in data) {
			element[k] = data[k];
		}
	}

	// webkit browsers don't currently support 'children' on SVG elements...
	function getChildElementCount(element) {
		if (element.children) {
			return element.children.length;
		}
		if (typeof element.childElementCount !== 'undefined') {
			return element.childElementCount;
		}
		var count = 0;
		for (var i = 0; i < element.childNodes.length; i++) {
			count += element.nodeType === 1 ? 1 : 0;
		}
		return count;
	}

	// translate an HTML subtree
	function translateFragment(element) {
		element = element || document.documentElement;

		// check all translatable children (= w/ a `data-l10n-id' attribute)
		var children = getTranslatableChildren(element);
		var elementCount = children.length;
		for (var i = 0; i < elementCount; i++) {
			translateElement(children[i]);
		}

		// translate element itself if necessary
		translateElement(element);
	}

	return {
		// get a localized string
		get : function(key, args, fallbackString) {
			var index = key.lastIndexOf('.');
			var prop = gTextProp;
			if (index > 0) { // An attribute has been specified
				prop = key.substr(index + 1);
				key = key.substring(0, index);
			}
			var fallback;
			if (fallbackString) {
				fallback = {};
				fallback[prop] = fallbackString;
			}
			var data = getL10nData(key, args, fallback);
			if (data && prop in data) {
				return data[prop];
			}
			return '{{' + key + '}}';
		},

		// debug
		getData : function() {
			return gL10nData;
		},
		getText : function() {
			return gTextData;
		},

		// get|set the document language
		getLanguage : function() {
			return gLanguage;
		},
		setLanguage : function(lang, callback) {
			loadLocale(lang, function() {
						if (callback)
							callback();
						translateFragment();
					});
		},

		// get the direction (ltr|rtl) of the current language
		getDirection : function() {
			// http://www.w3.org/International/questions/qa-scripts
			// Arabic, Hebrew, Farsi, Pashto, Urdu
			var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
			var shortCode = gLanguage.split('-', 1)[0];
			return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';
		},

		// translate an element or document fragment
		translate : translateFragment,

		// this can be used to prevent race conditions
		getReadyState : function() {
			return gReadyState;
		},
		ready : function(callback) {
			if (!callback) {
				return;
			} else if (gReadyState == 'complete'
					|| gReadyState == 'interactive') {
				window.setTimeout(function() {
							callback();
						});
			} else if (document.addEventListener) {
				document.addEventListener('localized', function once() {
							document.removeEventListener('localized', once);
							callback();
						});
			}
		}
	};
})(window, document);
